\chapter{使用Hyperscan库实现正则表达式编程}

与其它正则库的使用方式类似，Hyperscan也需要经过正则表达式编译、匹配和清理三个
主要步骤。

Hyperscan可以以单模的方式编译单个正则表达式，也可以以多模的方式编译多个正则
表达式。在匹配时，可以是简单的块匹配，也可以是流匹配，还可以是向量匹配。

\section{编译正则表达式}

Hyperscan库提供了3个API以实现单模、 多模和扩展多模正则表达式的编译。

\subsection{单模编译}

单模正则表达式编译的函数原型为：

\begin{minipage}{0.90\textwidth}
  \begin{ccode}
    hs_error_t hs_compile(const char *expression, unsigned int flags,
                          unsigned int mode,
                          const hs_platform_info_t *platform,
                          hs_database_t **db, hs_compile_error_t **error);
  \end{ccode}
\end{minipage}

该函数编译成功，返回\cinline{HS_SUCCESS}，失败返回\cinline{HS_COMPILER_ERROR}，
具体错误信息会写入在\cinline{error}指向的内存。

形参：
\begin{description}
  \item \cinline{expression}: 指向以\cinline{'\0'}结尾的正则表达式(不能包含类似
      \cinline{'/'}这样的定界符和\enquote{\cinline{/i}}这样的标志)。
  \item \cinline{flags}: 编译标志，可以通过\cinline{'|'}按位\enquote{或}运算设置
      多个标志组合(各个标志常量的详细说明见\enquote{\cinline{hs_compile.h}})，其有效值有:
    \begin{itemize}
      \item \cinline{HS_FLAG_CASELESS}：忽略匹配时的大小写
      \item \cinline{HS_FLAG_DOTALL}：\cinline{'.'}匹配时不排除换行符
      \item \cinline{HS_FLAG_MULTILINE}：在\cinline{'^'}和%
          \cinline{'$'}定界符中能够匹配换行符
      \item \cinline{HS_FLAG_SINGLEMATCH}：每个正则表达式只生成一个匹配项
      \item \cinline{HS_FLAG_ALLOWEMPTY}：允许与空字符串匹配的正则表达式，
          如\enquote{\cinline{.*}}
      \item \cinline{HS_FLAG_UTF8}：将正则表达式视为UTF-8字符序列
      \item \cinline{HS_FLAG_UCP}：字符类具备Unicode属性
      \item \cinline{HS_FLAG_PREFILTER}：在预过滤模式下编译正则表达式
      \item \cinline{HS_FLAG_SOM_LEFTMOST}：报告匹配时最左端的匹配偏移量
      \item \cinline{HS_FLAG_COMBINATION}：以逻辑组合语法解析正则表达式
      \item \cinline{HS_FLAG_QUIET}：忽略正则表达式的匹配报告(用于逻辑组合中的子表达式)
    \end{itemize}
  \item \cinline{mode}: 匹配模式标志，其有效值有:
    \begin{itemize}
      \item \cinline{HS_MODE_BLOCK}: 块模式
      \item \cinline{HS_MODE_STREAM}: 流模式
      \item \cinline{HS_MODE_VECTORED}: 向量模式
    \end{itemize}
  \item \cinline{platform}: 指定目标平台，如为\cinline{NULL}，表示生成适合于
      当前平台上运行的编译结果
  \item \cinline{db}: 编译成功时，指向生成的数据库，否则为\cinline{NULL}。
      注意：在使用后，需要调用\cinline{hs_free_database()}函数销毁编译生成的数据库。
  \item \cinline{error}: 指向\cinline{hs_compile_error_t}错误信息的指针，并提供
      错误的详细信息。注意：出错时，需要调用\cinline{hs_free_compile_error()}%
      释放该缓冲区。
\end{description}

\subsection{多模编译}

多模正则表达式编译是指将一组正则表达式编译到一个正则数据库，并且可以分别为每
一个正则表达式指定编号和编译标志，其函数原型为：

\begin{minipage}{0.90\textwidth}
  \begin{ccode}
    hs_error_t hs_compile_multi(const char *const *expressions,
                                const unsigned int *flags,
                                const unsigned int *ids,
                                unsigned int elements, unsigned int mode,
                                const hs_platform_info_t *platform,
                                hs_database_t **db,
                                hs_compile_error_t **error);
  \end{ccode}
\end{minipage}

该函数编译成功，返回\cinline{HS_SUCCESS}，失败返回\cinline{HS_COMPILER_ERROR}，
具体错误信息会写入在\cinline{error}指向的内存。

形参：
\begin{description}
  \item \cinline{expressions}: 与单模编译一样，指向以\cinline{'\0'}结尾的正则
      表达式构成的正则表达式指针数组的指针(二级指针)。
  \item \cinline{flags}: 指向标志数组的指针，可分别为多模正则表达式中的每个
      正则表达式指定不同编译标志。如传入的是\cinline{NULL}，则将所有正则
      表达式的编译标志设置为\cinline{0}，其有效值与单模编译相同。
  \item \cinline{ids}: 指向与\cinline{expressions}指针数组中各正则表达式相关联
      ID号数组的指针。如传入的是\cinline{NULL}，则将所有正则表达式的ID号设置为%
      \cinline{0} 。
  \item \cinline{elements}: 正则表达式数组中的元素个数。
  \item 其它与单模编译的形参相同。
  % \item \cinline{mode}: 与单模编译相同。
  % \item \cinline{platform}: 与单模编译相同。
  % \item \cinline{db}: 与单模编译相同。
  % \item \cinline{error}: 与单模编译相同。
\end{description}

\subsection{扩展多模编译}

与多模正则表达式编译相同，但允许为每个正则表达式通过\cinline{hs_expr_ext_t}指定
其它参数，其函数原型为：

\begin{minipage}{0.90\textwidth}
  \begin{ccode}
    hs_error_t hs_compile_ext_multi(const char *const *expressions,
                          const unsigned int *flags,
                          const unsigned int *ids,
                          const hs_expr_ext_t *const *ext,
                          unsigned int elements, unsigned int mode,
                          const hs_platform_info_t *platform,
                          hs_database_t **db, hs_compile_error_t **error);
  \end{ccode}
\end{minipage}

该函数编译成功，返回\cinline{HS_SUCCESS}，失败返回\cinline{HS_COMPILER_ERROR}，
具体错误信息会写入在\cinline{error}指向的内存。

形参：
\begin{description}
  \item \cinline{ext}: 指向\cinline{hs_expr_ext_t}结构的指针数组的指针，
      可以通过该结构为每个正则表达式指定扩展行为。如果都不需要扩展行为，
      则可以指定为\cinline{NULL}。如果某一个正则表达式不需要扩展行为，
      则可以指定指针数组中对应指针为\cinline{NULL}。
  \item 其它形参与\cinline{hs_compile_multi()}的形参相同。
\end{description}

\subsection{释放正则数据库资源}

在完成匹配扫描后，需要释放编译后的正则数据库，其函数原型为：

\begin{minipage}{0.90\textwidth}
  \begin{ccode}
    hs_error_t hs_free_database(hs_database_t *db);
  \end{ccode}
\end{minipage}

该函数释放成功，返回\cinline{HS_SUCCESS}，失败返回其它值。

形参：
\begin{description}
  \item \cinline{db}: 指向正则数据库的指针。
\end{description}

\section{临时存储空间}

在匹配时为每个线程或并发调用提供必须的暂存空间，需要在匹配之前为匹配函数准备
临时存储空间，并在完成匹配后释放临时存储空间。

\subsection{分配临时存储空间}

分配临时存储空间的函数原型为：

\begin{minipage}{0.90\textwidth}
  \begin{ccode}
    hs_error_t hs_alloc_scratch(const hs_database_t *db,
                                hs_scratch_t **scratch);
  \end{ccode}
\end{minipage}

该函数分配成功返回\cinline{HS_SUCCESS}，分配失败返回\cinline{HS_NOMEM}。
如果指定了无效参数，则可能返回其它错误。

形参：
\begin{description}
  \item \cinline{db}: 指向正则数据库编译结果的指针。
  \item \cinline{scratch}: 指向临时空间指针的指针(二级指针)。首次分配时，应为
      其提供\cinline{NULL}，以便可以分配新的临时内存。如果已分配，则查看该临时
      内存对\cinline{db}指向的正则数据库是否有效。如果需要新的临时内存，则将
      释放原内存，并指向新分配的临时内存地址，否则将指向原临时内存。分配成功后，
      除了原临时内存空间适配的正则数据库外，还适用于所提供编译结果。
\end{description}

说明：任何使用\cinline{hs_set_scratch_allocator()}或%
\cinline{hs_set_allocator()}设置的内存分配回调函数都可以用于该函数。

\subsection{释放临时存储空间}

释放临时存储空间的函数原型为：

\begin{minipage}{0.90\textwidth}
  \begin{ccode}
    hs_error_t hs_free_scratch(hs_scratch_t *scratch);
  \end{ccode}
\end{minipage}

该函数释放成功返回\cinline{HS_SUCCESS}，失败返回其它值。

形参：
\begin{description}
  \item \cinline{scratch}: 指向临时空间的指针。
\end{description}

说明：任何使用\cinline{hs_set_scratch_allocator()}或%
\cinline{hs_set_allocator()}设置的内存分配回调函数都可以用于该函数。

\section{实现正则匹配}

Hyperscan支持块模式(Block Mode)、流模式(Streaming Mode)和向量模式(Vectored Mode)
多种正则匹配模式。

\subsection{块模式}

在块模式中，会对一个完整数据块进行扫描匹配，扫描结束即返回匹配结果。也就是说，
所有匹配结果都位于该段完整的数据块中，不跨越数据块，这是一种传统的匹配模式。
块模式在所有模式中，消耗资源最小，匹配效率最高。

执行块扫描匹配的函数原型为：

\begin{minipage}{0.90\textwidth}
  \begin{ccode}
    hs_error_t hs_scan(const hs_database_t *db,
                       const char *data,
                       unsigned int length, unsigned int flags,
                       hs_scratch_t *scratch,
                       match_event_handler onEvent,
                       void *context);
  \end{ccode}
\end{minipage}

该函数扫描成功返回\cinline{HS_SUCCESS}(不代表匹配成功)，扫描失败返回其它值。

形参：
\begin{description}
  \item \cinline{db}: 指向正则数据库的指针；
  \item \cinline{data}: 指向需要扫描匹配的块数据指针；
  \item \cinline{length}: 需要扫描匹配的块数据长度；
  \item \cinline{flags}: 预留，暂未使用；
  \item \cinline{scratch}: 指向临时存储空间的指针，多线程扫描时，每个线程都需要
      自己独立的临时存储空间；
  \item \cinline{onEvent}: 指向匹配回调函数指针，每匹配成功一次就会触发该回调函数的
      一次调用；
  \item \cinline{context}: 指向传入\cinline{onEvent}指向的回调函数的参数的指针，
      通过该指针，可以在回调函数中将匹配结果等内容结果写入该指针指向的空间。
\end{description}

\subsection{流模式}

当被扫描数据不在一段完整的数据块中时，例如在网络场景下，数据是分散在持续不断
的数据包中的(数据流)，扫描结果往往会跨两个或多个数据包，在不同的数据包中都有
一部分匹配内容。为此，Hyperscan专门设计为对这种流数据匹配设置了流模式，通过
有限量流内存对流匹配信息进行记录，在不缓存已过去流数据的情况下，保证能正确匹配
出所有结果，以实现实时匹配。

当然，也可以采用传统方案，先把所有数据全部缓存，再执行一次匹配。但是这样的不仅
会增加内存开销，还会丢失匹配的实时性，因为数据流可能会持续很长的一段时间。

与流模式相关的主要流打开函数、流扫描函数和流关闭函数。

\subsubsection{流打开函数}

流打开函数的函数原型为：

\begin{minipage}{0.90\textwidth}
  \begin{ccode}
    hs_error_t hs_open_stream(const hs_database_t *db, unsigned int flags,
                              hs_stream_t **stream);
  \end{ccode}
\end{minipage}

该函数流打开成功返回\cinline{HS_SUCCESS}(不代表匹配成功)，失败返回其它值。

形参：
\begin{description}
  \item \cinline{db}: 指向正则数据库的指针；
  \item \cinline{flags}: 预留，暂未使用；
  \item \cinline{stream}: 指向数据流指针的指针(二级指针)；
\end{description}

\subsubsection{流扫描函数}

流扫描函数的函数原型为：

\begin{minipage}{0.90\textwidth}
  \begin{ccode}
    hs_error_t hs_scan_stream(hs_stream_t *id, const char *data,
                              unsigned int length, unsigned int flags,
                              hs_scratch_t *scratch,
                              match_event_handler onEvent, void *ctxt);
  \end{ccode}
\end{minipage}

该函数扫描成功返回\cinline{HS_SUCCESS}(不代表匹配成功)，扫描失败返回其它值。

形参：
\begin{description}
  \item \cinline{id}: \cinline{hs_open_stream()}函数返回的数据流；
  \item 其它参数与块模式扫描函数形参相同。
\end{description}

\subsubsection{流关闭函数}

流关闭函数的函数原型为：

\begin{minipage}{0.90\textwidth}
  \begin{ccode}
    hs_error_t hs_close_stream(hs_stream_t *id, hs_scratch_t *scratch,
                               match_event_handler onEvent, void *ctxt);
  \end{ccode}
\end{minipage}

该函数流关闭成功返回\cinline{HS_SUCCESS}，失败返回其它值。

形参：
\begin{description}
  \item \cinline{id}: 指向\cinline{hs_open_stream()}函数返回的数据流的指针；
  \item \cinline{scratch}: 指向\cinline{hs_alloc_scratch()}函数分配的临时空间的指针；
  \item \cinline{onEvent}: 指向匹配回调函数指针，每匹配成功一次就会触发该回调函数的
      一次调用。如传入\cinline{NULL}则不返回匹配结果；
  \item \cinline{ctxt}: 指向传入\cinline{onEvent}指向的回调函数的参数的指针，
      通过该指针，可以在回调函数中将匹配结果写入该指针指向的内存。
\end{description}

\subsection{向量模式}

向量模式适用以下场景：
\begin{description}
  \item 要匹配的数据块分散在多个不连续的内存块中；
  \item 这些不连续的数据，不像流数据在时间上有先后，是在扫描前就全部准备好的。
\end{description}

向量扫描的函数原型为：

\begin{minipage}{0.90\textwidth}
  \begin{ccode}
    hs_error_t hs_scan_vector(const hs_database_t *db,
                              const char *const *data,
                              const unsigned int *length,
                              unsigned int count, unsigned int flags,
                              hs_scratch_t *scratch,
                              match_event_handler onEvent, void *context);
  \end{ccode}
\end{minipage}

该函数扫描成功返回\cinline{HS_SUCCESS}(不代表匹配成功)，扫描失败返回其它值。

形参：
\begin{description}
  \item \cinline{data}: 指向需要扫描匹配的数据的指针数组的指针(二级指针)；
  \item \cinline{length}: 指向存储每个需要扫描匹配的数据长度的数组的指针；
  \item \cinline{count}: 需要扫描匹配的数据指针数组的大小，与length指向的数组
      的大小相同；
  \item 其它与块扫描函数的形参相同。
\end{description}

\section{扫描回调函数}

扫描回调函数的函数类型定义为：

\begin{minipage}{0.90\textwidth}
  \begin{ccode}
    typedef int (*match_event_handler)(unsigned int id,
                                       unsigned long long from,
                                       unsigned long long to,
                                       unsigned int flags,
                                       void *context);
  \end{ccode}
\end{minipage}

该函数返回0表示继续扫描，返回非0值表示结束扫描。

形参：
\begin{description}
  \item \cinline{id}: 匹配的正则表达式ID号。如果是单模匹配，则永远是0；
  \item \cinline{from}: 如果为当前正则表达式启用了\cinline{HS_FLAG_SOM_LEFTMOST}%
      匹配开始标志，那么该参数将记录匹配起始值，但前提是该值位于由 %
      \cinline{SOM_HORIZON} 模式标志之一设置的\enquote{匹配起始值范围}内。
      如果该匹配起始值位于该范围之外(只有当%
      \cinline{SOM_HORIZON}的值不是\cinline{HS_MODE_SOM_HORIZON_LARGE}时
      才发生)，则\cinline{from}被设置为\cinline{HS_OFFSET_PAST_HORIZON}。如果
      未启用匹配开始标志，则此参数将被设置为0。启用匹配开始标志会降低扫描
      效率；
  \item \cinline{to}: 与正则表达式匹配的最后一个字符之后的一个字符的偏移量；
  \item \cinline{flags}: 预留，暂未使用；
  \item \cinline{context}: 在\cinline{hs_scan()}、\cinline{hs_scan_vector()}和
      \cinline{hs_scan_stream()}扫描函数中提供的指向用户提供的数据的指针。
\end{description}

必须为\cinline{hs_scan()}、\cinline{hs_scan_vector()}和 \cinline{hs_scan_stream()}%
扫描函数(或其它可能产生匹配结果的函数)提供与该类型定义相匹配的回调函数。

在执行扫描过程中，只要出现匹配，就会触发该回调函数的调用。匹配结果数据将作为
参数传递给回调函数，回调函数应返回一个值，表明是否应继续在目标数据上进行匹配。
如果扫描调用不需要回调函数，则可以提供\cinline{NULL}实参以抑制匹配结果。

在该回调函数中，不应为同一数据流调用Hyperscan API函数，也不应重复使用为会触发
回调函数的API函数分配的临时空间。使用完全独立的参数再次调用Hyperscan库是可行的
(例如，在新的数据流中使用新的临时空间扫描不同的正则数据库)，但重复使用
流状态和(或)临时空间等数据结构可能会产生未定义的行为。

\section{Hyperscan正则编程样例}\label{sec-hyperscan-workflow}

由于可以支持流模式匹配，Hyperscan库与其它3个库的编程流程稍有不同：
\begin{enumerate}
    \item 编译正则表达式
    \item 分配临时存储空间
    \item 打开数据流(仅流模式需要)
    \item 扫描匹配
    \item 释放临时存储空间
    \item 关闭数据流(仅流模式需要)
    \item 释放正则数据库
\end{enumerate}

当然，在使用中，还应该对各类错误做出响应，以给用户提供清晰的反馈。

% 说明：本节中的代码摘录自\url{https://www.bilibili.com/read/cv22740260/?from=search&spm_id_from=333.337.0.0}，%
% 在此仅对个别内容进行了简单调整。

\subsection{单模块扫描}

单模块扫描使用样例如代码\ref{code-base-block}所示。

\cvfile[label=code-base-block]{单模块扫描匹配}
{intel-Hyperscan/codes/tests/01workflow/workflow.c}

在代码\ref{code-base-block}中的第36行，声明了一个整型变量\cinline{flag}，并在
  第38---39行调用\cinline{hs_scan()}函数时，通过\cinline{&flag}取得该变量的地址后作为
实参传入扫描函数。该地址会在触发\cinline{on_match} 指向的回调函数调用时，被传入
回调函数。在\cinline{on_match()}回调函数中，第59行将传入的\cinline{void*}%
指针强制转换为\cinline{int*}指针，然后在第60行为该指针指向的变量\cinline{flag}%
进行赋值。从而记录是否发生匹配的状态。

\subsection{单模\texttt{stdin}流扫描}

采用单模从标准输入流\cinline{stdin}中实现流扫描使用样例如代码%
\ref{code-base-stdin-stream}所示。

\cvfile[label=code-base-stdin-stream]{单模\cinline{stdin}流块扫描匹配}%
  {intel-Hyperscan/codes/tests/02stdin-stream-match/stdin-stream-match.c}

在代码\ref{code-base-stdin-stream}中的第54行，将正则表达式的指针%
\cinline{pat}作为实参传入\cinline{hs_scan_stream()}扫描函数。它会在触发%
\cinline{on_match}指向的回调函数调用时，被传入回调函数。在%
\cinline{on_match()} 回调函数中，第70行将传入的\cinline{void*}指针强制转换为\cinline{char*}指针，
然后在第72行通过该指针输出匹配所使用的正则表达式。

\subsection{单模文件流扫描}

采用单模从文件流中实现流扫描使用样例如代码%
\ref{code-base-file-stream}所示。

\cvfile[label=code-base-file-stream]{单模文件流扫描匹配}%
  {intel-Hyperscan/codes/tests/03file-stream-match/file-stream-match.c}

在代码\ref{code-base-file-stream}中的第63行，将正则表达式的指针%
\cinline{pat}作为实参传入\cinline{hs_scan_stream()}扫描函数。它会在触发%
\cinline{on_match}指向的回调函数调用时，被传入回调函数。在\cinline{on_match()}%
回调函数中，第88行将传入的\cinline{void*}指针强制转换为\cinline{char*}指针，
然后在第90行通过该指针输出匹配所使用的正则表达式。

\subsection{单模向量扫描}

采用单模向量扫描使用样例如代码\ref{code-base-vector}所示。

\cvfile[label=code-base-vector]{单模向量扫描匹配}%
  {intel-Hyperscan/codes/tests/04vector-match/vector-match.c}

\subsection{单模文件块扫描}

采用单模在文件中实现块扫描使用样例如代码\ref{code-base-file-block}所示。

\cvfile[label=code-base-file-block]{单模在文件中实现块扫描匹配}%
  {intel-Hyperscan/codes/tests/05match-in-file/match-in-file.c}

在代码\ref{code-base-file-block}中的第61行，用被扫描字符数组\cinline{chunk[]}%
数组的数组名(首地址)作为实参传入\cinline{hs_scan()}扫描函数。该地址会在触发%
\cinline{on_match}指向的回调函数调用时，被传入回调函数。在\cinline{on_match()}%
回调函数中，第84行将传入的\cinline{void*}指针强制转换为\cinline{char*}指针，
然后在第86行通过该指针输出该指针指向的被匹配字符串。

\subsection{多模块扫描}

采用多模块扫描使用样例如代码\ref{code-multi-block}所示。

\cvfile[label=code-multi-block]{多模块扫描匹配}%
  {intel-Hyperscan/codes/tests/06multi-block-match/multi-block-match.c}

在代码\ref{code-multi-block}中的第53行，将多模正则表达式数组%
\cinline{patterns[]}的数组名(首地址，地址的地址)作为实参传入%
\cinline{hs_scan()}扫描函数。该地址会在触发\cinline{on_match}指向的回调函数
调用时，被传入回调函数。在\cinline{on_match()}回调函数中，第72行将传入的%
\cinline{void*}指针强制转换为\cinline{char**}指针，然后在第74行，用发生匹配的
正则表达式的id号作为下标，输出正则表达式数组中使用的正则表达式。

\subsection{多模块扫描的封装}

采用再次封装的多模块扫描使用样例如代码\ref{code-multi-block-global}所示。

\cvfile[label=code-multi-block-global]{再封装多模块扫描匹配}%
  {intel-Hyperscan/codes/tests/07multi-block-match-global/multi-block-match-global.c}

在代码\ref{code-multi-block-global}中，第9---15行定义了表示正则表达式ID号的枚举
常量。第18---22行，定义了用于存储一条正则表达式内容和编号的结构体。第25---30行
定义了用于存储多模正则表达式编号、标志和正则表达式内容的结构体。

第33---38行定义了用于多模式扫描匹配的正则表达式结构体数组，注意将数组最后一个
元素的str成员赋值为\cinline{NULL}以作为数组结束标志。

为便于功能划分和管理，分别设计了\cinline{do_compile()}、\cinline{get_scratch()}和%
\cinline{do_scan()}函数对Hyperscan的API函数进行了再次封闭。

在驱动/测试函数\cinline{main()}函数中，第59---64行通过遍历全局数组%
\cinline{gPattern}实现对多模结构各成员的赋值，注意在该循环中，使用%
\cinline{NULL}作为循环结束条件。

第81---88行，构造了一个以\cinline{NULL}结束的字符串数组，并遍历该数组，对每个
字符串实现扫描匹配。在封装的\cinline{do_scan()}函数中，第145行，将传入的%
\cinline{input}指针传入了\cinline{hs_scan()}扫描函数。该指针会在触发%
\cinline{on_match}指向的回调函数调用时，被传入回调函数。在\cinline{on_match()}%
回调函数中，第159行将传入的\cinline{void*}指针强制转换为\cinline{char*}指针，
在第163行，用发生匹配的正则表达式的id号作为下标输出全局数组中指定的正则表达式，
在第165行，通过\cinline{"%.*s"}输出被扫描匹配字符串匹配成功的子字符串。

由于篇幅所限，有关多模的流扫描和向量扫描及文件扫描，请参阅单模示例代码，
在此不再赘述。
